package org.restlesscode.javersion;

import java.util.List;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.util.ArrayList;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.tmatesoft.svn.core.SVNCommitInfo;
import org.tmatesoft.svn.core.SVNException;
import org.tmatesoft.svn.core.SVNNodeKind;
import org.tmatesoft.svn.core.SVNPropertyValue;
import org.tmatesoft.svn.core.io.ISVNEditor;
import org.tmatesoft.svn.core.io.diff.SVNDeltaGenerator;

/**
 * Writes objects into a subversion repository.
 */
public class SvnObjectWriter {

	private static final Log LOG = LogFactory.getLog(SvnObjectWriter.class);

    protected static final String VERSION_FIELD = "jvn.class.version";
    protected static final String TRANSCODER_NAME_FIELD = "jvn.transcoder.name";

	protected SvnStore svnStore;

    /**
     * Create a writer object from the given SvnStore.
     * @param svnStore pointer to Svn repository storing desired objects
     */
	public SvnObjectWriter(SvnStore svnStore) {
		this.svnStore = svnStore;
	}

    /**
     * Write the given object to the supplied path using the default transcoder.
     * Directories will be created as necessary.
     * @param path Path to write to
     * @param o Object to write
     * @throws IOException
     */
    public synchronized void write(String path, Object o) throws IOException {
        write(path, o, this.svnStore.getDefaultTranscoder());
    }

	/**
	 * Writes an object to an SVN repository at the given path. Directories will
	 * be created as necessary. Object need not be written already.
	 * @param path Path relative to SVN root to save object at
	 * @param o Object to save
	 * @throws IOException Serialization problem or problem with object
	 */
	public synchronized <T> void write(String path, T o, Transcoder<T> transcoder) throws IOException {

        SvnObject svnObject = transcoder.encode(o);
		
		try {
			svnStore.commitLock.acquire();
			writeToSvn(path, svnObject);
		} catch (Throwable throwable) {
			throw new IOException(throwable);
		} finally {
			svnStore.commitLock.release();
		}
	}
	
	protected void writeToSvn(String path, SvnObject svnObject)
            throws SVNException, IOException {
		
		boolean fileExistsInSvn = checkFileExists(path);
		LOG.info("Saving to path: " + path);

		List<String> pathsToCreate = findPathsToCreate(path);

        // Create directories if necessary
        if (pathsToCreate.size() > 0) {
            ISVNEditor editor = svnStore.repository.getCommitEditor("Creating directories", null);
            editor.openRoot(SvnRevision.HEAD);

            for (String p : pathsToCreate) {
                editor.addDir(p, null, -1);
            }

            editor.closeDir();
            SVNCommitInfo commitInfo = editor.closeEdit();
            LOG.info("Commited revision = " + commitInfo.getNewRevision());
        }
        
        ISVNEditor editor = svnStore.repository.getCommitEditor("Saving object", null);
        editor.openRoot(SvnRevision.HEAD);

		if (fileExistsInSvn) {
			editor.openFile(path, -1);
		} else {
			editor.addFile(path, null, -1);
		}
		
		String checksum = null;
		
		if (svnObject.getContent() != null) {
			editor.applyTextDelta(path, null);
			SVNDeltaGenerator deltaGenerator = new SVNDeltaGenerator();

            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            baos.write(svnObject.getContent());
            checksum = deltaGenerator.sendDelta(path, new ByteArrayInputStream(baos.toByteArray()), editor, true);
		}
		editor.changeFileProperty(path, VERSION_FIELD, SVNPropertyValue.create(svnObject.getVersion()));
		editor.changeFileProperty(path, TRANSCODER_NAME_FIELD, SVNPropertyValue.create(svnObject.getTranscoderName()));
		
		for (String key : svnObject.getProperties().keySet()) {
			SvnPropertyObject objToStore = svnObject.getProperties().get(key);
			switch (objToStore.getPropertyType()) {
				case STRING:
					editor.changeFileProperty(path, key,
							SVNPropertyValue.create(objToStore.getValueAsString()));
					break;
				case OBJECT:
					LOG.info("Serializing " + objToStore);
					ByteArrayOutputStream baos = new ByteArrayOutputStream();
					ObjectOutputStream oos = new ObjectOutputStream(baos);
					oos.writeObject(objToStore.getValue());
					oos.flush();
					editor.changeFileProperty(path, key,
							SVNPropertyValue.create(key, baos.toByteArray()));
					break;
			}
		}
		
		editor.closeFile(path, checksum);

		editor.closeDir();
		SVNCommitInfo commitInfo = editor.closeEdit();
		LOG.info("Commited object revision = " + commitInfo.getNewRevision());
	}

	protected List<String> findPathsToCreate(String path) throws SVNException, IOException {
		
		List<String> pathsToCreate = new ArrayList<String>();
		
		String pathElements[] = path.split("/");
		String currPath = "";
		
		for (int i = 0; i < pathElements.length - 1; i++) {
			String pathElement = pathElements[i];
			currPath += (i > 0 ? "/" : "") + pathElement;
			SVNNodeKind nodeKind = svnStore.repository.checkPath(currPath, -1);
			if (nodeKind == SVNNodeKind.NONE) {
				pathsToCreate.add(currPath);
			} else if (nodeKind == SVNNodeKind.FILE) {
				throw new IOException("Error: cannot make directory, file exists at path " + currPath);
			}
		}
		
		return pathsToCreate;
	}

	protected boolean checkFileExists(String path) throws SVNException {
		return (svnStore.repository.checkPath(path, -1) == SVNNodeKind.FILE);
	}

	
}
